package uci

import (
	"bufio"
	pos "code.google.com/p/gochess/position"
	"code.google.com/p/gochess/search"
	stats "code.google.com/p/gochess/statistics"
	"fmt"
	"io"
	"strconv"
	"strings"
	"time"
)

var flagOptions = map[string]uint32{
	"Hash Table":          search.NoTT,
	"Quiescence Search":   search.NoCutOffs,
	"Late Move Reduction": search.NoLMR,
	"Killer Heuristic":    search.NoKiller,
}

// write to gui and to debug logger
type outWriter struct {
	out, log io.Writer
}

func (l *outWriter) Write(p []byte) (n int, err error) {
	if _, err = l.log.Write(append([]byte{'<', ' '}, p...)); err != nil {
		return n, err
	}
	return l.out.Write(p)
}

type parser struct {
	guiIn, info chan string
	out, logger io.Writer
	p           *pos.Position
	duration    time.Duration
	depth       int32
	flags       uint32
}

func (p *parser) reset() {
	p.info = make(chan string)
	p.depth = 0
	p.duration = 0
	// p.p = nil ? todo
}

type stateFn func(*parser) stateFn

// ignored commands: debug, register
func StartEngine(in io.Reader, o, logger io.Writer) {

	p := &parser{make(chan string), make(chan string),
		&outWriter{out: o, log: logger}, logger, nil, 0, 0, 0}
	go listenToGui(bufio.NewReader(in), p.guiIn)

	for state := wait; state != nil; {
		state = state(p)
	}
}

const maxTTSize = 2048

func wait(p *parser) stateFn {
	p.reset()
	for {
		guiCmd := <-p.guiIn
		fmt.Fprintln(p.logger, ">", guiCmd)
		cmd, params, err := parseCommandline(guiCmd)
		if err != nil {
			fmt.Fprintln(p.logger, ">", err)
			continue
		}
		switch cmd {
		case "uci":
			fmt.Fprintln(p.out, uciResponse)
		case "isready":
			fmt.Fprintln(p.out, `readyok`)
		case "setoption":
			name, value, err := parseSetoption(params)
			if err != nil {
				fmt.Fprintln(p.logger, ">", err)
				continue
			}
			switch name {
			case "Hash":
				size, err := strconv.Atoi(value)
				if err == nil && size >= 1 && size <= maxTTSize {
					search.Transpositions.Resize(size)
				}
			default:
				// last resort: a bool option
				for id := range flagOptions {
					if id == name {
						switch value {
						case "false":
							p.flags |= flagOptions[id]
						case "true":
							p.flags &= ^flagOptions[id]
						}
						break
					}
				}
			}
		case "ucinewgame":
			search.Transpositions.Clear()
			p.p = nil
		case "position":
			if p.p, err = parsePosition(params); err != nil {
				fmt.Fprintln(p.logger, ">", err)
				continue
			}
		case "go":
			if p.p == nil {
				// no position set so far
				fmt.Fprintln(p.logger, "> error: position not set")
				continue
			}
			doThink := true
			switch {
			case strings.HasPrefix(params, "ponder"): // skip search for now
				doThink = false
			case strings.HasPrefix(params, "infinite"): // nothing to do
			case strings.HasPrefix(params, "searchmoves"): // todo:
			default:
				// only left: key-value pairs with integer values
				params, err := parseDigitParams(params)
				if err != nil {
					fmt.Fprintln(p.logger, ">", err)
					continue
				}
				var failure bool
				for i := range params {
					switch value := params[i].value; params[i].key {
					case "depth":
						p.depth = value
					case "movetime":
						p.duration = time.Duration(value) * time.Millisecond
					case "wtime":
						if p.p.SideToMove() == pos.White {
							p.duration = time.Duration(value) * time.Millisecond / 25
						}
					case "btime":
						if p.p.SideToMove() == pos.Black {
							p.duration = time.Duration(value) * time.Millisecond / 25
						}
					case "winc": // todo
					case "binc": // todo
					case "nodes": // todo
					case "mate": // todo
					case "movestogo": // ignore for now
					default:
						fmt.Fprintln(p.logger, "> unknown/invalid 'go' option")
						failure = false
						break
					}
				}
				if failure {
					continue
				}
			}
			if doThink {
				return think
			}
		case "ponderhit": // ignore for now
		case "quit":
			stats.StopTimer()
			return nil
		default:
			fmt.Fprintln(p.logger, "> unknown/invalid command")
		}
	}
}

func think(p *parser) stateFn {
	stats.ClearStatistics()
	stats.StartTimer()
	search.RunUCISearch(p.p, pos.NoMove, p.duration, p.depth, p.flags, p.info)
	for {
		select {
		case guiCmd := <-p.guiIn:
			fmt.Fprintln(p.logger, ">", guiCmd)
			cmd, _, err := parseCommandline(guiCmd)
			if err != nil {
				fmt.Fprintln(p.logger, ">", err)
				continue
			}
			switch cmd {
			case "uci":
				fmt.Fprintln(p.out, uciResponse)
			case "isready":
				fmt.Fprintln(p.out, `readyok`)
			case "stop":
				stats.StopTimer()
				return stop
			case "quit":
				stats.StopTimer()
				return quit
			}
		case line, ok := <-p.info:
			if !ok {
				// channel was closed, so search is finnished
				return wait
			}
			fmt.Fprintln(p.out, line)
		}
	}
}

func stop(p *parser) stateFn {
	search.Stop()
	for line := range p.info {
		fmt.Fprintln(p.out, line)
	}
	return wait
}

func quit(p *parser) stateFn {
	search.Stop()
	for _ = range p.info { // discard output
	}
	return nil
}

var uciResponse string

func init() {

	var flags string
	for id := range flagOptions {
		flags += fmt.Sprintf("option name %s type check default true\n", id)
	}
	uciResponse = fmt.Sprintf(`id name gochess 0.5
id author Heiko Schlichting
option name Hash type spin default %d min 1 max %d
%suciok`, search.TTDefaultSize, maxTTSize, flags)

}

func listenToGui(in *bufio.Reader, out chan<- string) {
	for {
		line, err := in.ReadString('\n')
		if err != nil {
			// You may check here if err == io.EOF
			// todo: what to do here? ignore?
		}
		out <- line[:len(line)-1]
	}
}
